package org.fitweb.runners;

import org.junit.runner.Runner;
import org.junit.runner.notification.RunNotifier;
import org.junit.runners.BlockJUnit4ClassRunner;
import org.junit.runners.Suite;
import org.junit.runners.model.FrameworkMethod;
import org.junit.runners.model.InitializationError;
import org.junit.runners.model.Statement;
import org.junit.runners.model.TestClass;

import java.lang.annotation.*;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;

/**
 * Copied from org.junit.runners.Parameterized in JUnit 4.8.2 and extended to allow optional renaming of each test,
 * so that:<pre>
 * myFruitRelatedTest[0]
 * myFruitRelatedTest[1]
 * ...
 * myFruitRelatedTest[n]
 * </pre>
 * becomes:<pre>
 * oranges
 * apples
 * ...
 * pears
 * </pre>
 * <p/>
 * create a public static String
 */
public class Parameterized extends Suite {
    /**
     * Annotation for a method which provides parameters to be injected into the
     * test class constructor by <code>Parameterized</code>
     */
    @Retention(RetentionPolicy.RUNTIME)
    @Target(ElementType.METHOD)
    public static @interface Parameters {
    }

    /**
     * Annotation for a method which can generate a meaningful test name from a single set of parameters
     */
    @Retention(RetentionPolicy.RUNTIME)
    @Target(ElementType.METHOD)
    public static @interface TestRunName {
    }

    private class TestClassRunnerForParameters extends BlockJUnit4ClassRunner {
        private final int fParameterSetNumber;

        private final List<Object[]> fParameterList;

        TestClassRunnerForParameters(Class<?> type, List<Object[]> parameterList, int i) throws InitializationError {
            super(type);
            fParameterList = parameterList;
            fParameterSetNumber = i;
        }

        @Override
        public Object createTest() throws Exception {
            return getTestClass().getOnlyConstructor().newInstance(computeParams());
        }

        private Object[] computeParams() throws Exception {
            try {
                return fParameterList.get(fParameterSetNumber);
            } catch (ClassCastException e) {
                throw new Exception(String.format("%s.%s() must return a Collection of arrays.", getTestClass().getName(), getAnnotatedMethod(getTestClass(), Parameters.class).getName()));
            }
        }

        @Override
        protected String getName() {
            return String.format("[%s]", fParameterSetNumber);
        }

        @Override
        protected String testName(final FrameworkMethod method) {
            return getTestName(getTestClass(), fParameterList.get(fParameterSetNumber));
        }

        @Override
        protected void validateConstructor(List<Throwable> errors) {
            validateOnlyOneConstructor(errors);
        }

        @Override
        protected Statement classBlock(RunNotifier notifier) {
            return childrenInvoker(notifier);
        }
    }

    private final ArrayList<Runner> runners = new ArrayList<Runner>();

    /**
     * Only called reflectively. Do not use programmatically.
     */
    public Parameterized(Class<?> klass) throws Throwable {
        super(klass, Collections.<Runner>emptyList());
        List<Object[]> parametersList = getParametersList(getTestClass());
        for (int i = 0; i < parametersList.size(); i++) {
            runners.add(new TestClassRunnerForParameters(getTestClass().getJavaClass(), parametersList, i));
        }
    }

    @Override
    protected List<Runner> getChildren() {
        return runners;
    }

    @SuppressWarnings("unchecked")
    private List<Object[]> getParametersList(TestClass klass) throws Throwable {
        return (List<Object[]>) getAnnotatedMethod(klass, Parameters.class).invokeExplosively(null);
    }

    @SuppressWarnings("unchecked")
    private String getTestName(TestClass klass, Object[] parameters) {
        try {
            FrameworkMethod method = getAnnotatedMethod(klass, TestRunName.class);
            return (String) method.invokeExplosively(null, (Object) parameters[0]);
        } catch (Throwable throwable) {
            throw new RuntimeException(throwable);
        }
    }

    private FrameworkMethod getAnnotatedMethod(TestClass testClass, Class<? extends Annotation> annotation) {
        List<FrameworkMethod> methods = testClass.getAnnotatedMethods(annotation);
        for (FrameworkMethod each : methods) {
            int modifiers = each.getMethod().getModifiers();
            if (Modifier.isStatic(modifiers) && Modifier.isPublic(modifiers)) return each;
        }

        throw new RuntimeException("No public static @" + annotation.getSimpleName() + " method on class " + testClass.getName());
    }
}
